package com.feifan.consumer.aop;

import com.alibaba.dubbo.config.annotation.Reference;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.annotation.NoClass;
import com.feifan.api.annotation.SystemLog;
import com.feifan.api.model.UmsAdmin;
import com.feifan.api.model.UmsLog;
import com.feifan.api.service.ITokenService;
import com.feifan.api.service.IUserLogService;
import com.feifan.api.service.IUserService;
import com.feifan.common.IpInfoUtil;
import com.feifan.common.ThreadPoolUtil;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * @program: FinalProject
 * @author: Xiaofan
 * @createTime: 2021-08-05 09:09
 * @description: Functions of this class is
 **/
@Aspect//表示该是一个切面
@Component//组件
@Slf4j//日志
public class SystemLogAspect implements HandlerInterceptor {
    //dobbo做法
    @Reference(
            version = "1.0.0",
            interfaceName = "com.feifan.api.service.IUserService",
            interfaceClass = IUserService.class,
            timeout = 170000
            //这个timeout时间一定要设置的长一点，不然当连接不上了就直接报错了！！！
    )
    private IUserService service;

    //dobbo做法
    @Reference(
            version = "1.0.0",
            interfaceName = "com.feifan.api.service.ITokenService",
            interfaceClass = ITokenService.class,
            timeout = 170000//防止一下子找不到zookeeper就连接中断
    )
    private ITokenService tokenService;

    //dobbo做法
    @Reference(
            version = "1.0.0",
            interfaceName = "com.feifan.api.service.IUserLogService",
            interfaceClass = IUserLogService.class,
            timeout = 170000//防止一下子找不到zookeeper就连接中断
    )
    private IUserLogService logService;

    //是一个纯单线程的，线程内部的存储类，可以在指定线程内存储数据，数据存储后，只有指定的线程可以得到存储数据，保证了单线程
    private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal begin time");

    @Pointcut("@annotation(com.feifan.api.annotation.SystemLog)")//表示改切点为能用于括号里注解修饰的那些类
//    @Pointcut("execution(com.feifan.consumer.controller.*.*(..))")//而这个限制比较多，切点只能该类下的所有方法
    public void controllerAspect() {
        System.out.println("hello");
    }

    @Before("controllerAspect()")
    public void before(JoinPoint joinPoint) {
        Date date=new Date();
        beginTimeThreadLocal.set(date);//将本地线程开始时间设置为当前时间
    }

    @AfterReturning("controllerAspect()")
    public void after(JoinPoint joinPoint){
        try {
            ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request=attributes.getRequest();//得到其真正的从前端送过来的请求

            Map<String,String[]> requestParams=request.getParameterMap();//接收到从前端传来的字段数据
            //开始获取token
            String token=request.getHeader("Authorization");//通过该特殊的键去找到token
            if(token==null){
                throw new RuntimeException("token不存在，请重新登录！");
            }
            token = token.split("@")[1];//取出真正的token
            String userId = tokenService.getUserId(token);//通过token来取出userId
            //然后通过userId去找到用户
            UmsAdmin admin = service.findByUserId(userId);

            if (admin == null) {
                throw new RuntimeException("改用户不存在，请重新登录！");
            }
            //否则该用户存在
            if (tokenService.checkSign(token, admin.getPassword())) {//进行token登录验证

                //通过自己写的方法获得需求中的属性
                String description=getControllerMethodInfo(joinPoint).get("description").toString();
                String type=getControllerMethodInfo(joinPoint).get("type").toString();

                //进行Log存储
                UmsLog log=new UmsLog();
                Integer id=new Random().nextInt(100);//模拟多个用户来访问该界面
                log.setUserid(id);
                log.setLogType(Integer.parseInt(type));//返回日志类型
                log.setName(description);
                log.setRequestUrl(request.getRequestURI());//返回地址
                log.setRequestType(request.getMethod());//Get还是Post

                String rp=JSON.toJSONString(requestParams).replace(",","&&");//将里面的，替换为&&
                // 不然做flink分析的时候，数据无法分割
                log.setRequestParam(rp.replace("\"","\'"));//同时还要把里面的双引替换成单引

                log.setIp(IpInfoUtil.getIpAddr(request));//获取请求者的ip
                //在使用SimpleDateFormat时格式化时间的 yyyy.MM.dd 为年月日而如果希望格式化时间为12小时制的，则使用hh:mm:ss 如果希望格式化时间为24小时制的，则使用HH:mm:ss
                //妈的一开始用的是mm，搞了很久操
                log.setCreateTime(new SimpleDateFormat("yyyy-MM-dd").format(new Date()));//将日期格式改成yyyy-MM-dd的样子存进数据库
                log.setUpdateTime(new SimpleDateFormat("yyyy-MM-dd").format(new Date()));

                Long beginTime=beginTimeThreadLocal.get().getTime();//取出线程的开始时间
                Long endTime=System.currentTimeMillis();//获取线程的当前时间
                Long costTime=endTime-beginTime;//从而得到所消耗的时间
                log.setCostTime(Integer.parseInt(costTime.toString()));

                //进行持久化到mysql
                //运用线程池进行多线程存储

                //然后通过自己写的一个公共类去创建一个线程池来进行存储
                ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(log,logService));

            } else {
                throw new RuntimeException("token验证失败，请重新登录！");
            }
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }

    public  Map<String,Object> getControllerMethodInfo(JoinPoint joinpoint){
        Map<String,Object> map=new HashMap<>(16);
        //获取目标target的类名
        String targetName=joinpoint.getTarget().getClass().getName();
        //获取方法名
        String methodName=joinpoint.getSignature().getName();
        //获取形参
        Object args[]=joinpoint.getArgs();

        //通过反射来生成类对象
        try {
            Class targetClass=Class.forName(targetName);//获取该类
            Method method[]=targetClass.getMethods();//然后获取该类的所有方法

            for(Method m:method){
//                System.out.println(m.getName()+"-"+m.getParameterTypes().length);
                if(m.getName().equals(methodName)&&m.getParameterTypes().length==args.length){//判断形参个数是为了防止找到的是重载的方法

                    //如果找到方法与前面获取的方法名字相同的，且其形参个数相等的话，即找到了该方法
                    String description=m.getAnnotation(SystemLog.class).description();//然后就可以取出该注解的属性
                    Integer type=m.getAnnotation(SystemLog.class).type().ordinal();//即取出该注解第二个属性中枚举类型再远类中的的索引号
                    map.put("description",description);
                    map.put("type",type);
                }
            }
        }
        catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        return map;
    }
}